iT邦幫忙

1

使用 Angular、Veo 3.1 與 Firebase 雲端函式 建立影片生成管線

  • 分享至 

  • xImage
  •  

在現代網頁應用程式中,將繁重的生成式人工智慧(Generative AI)邏輯從前端卸載到後端是必要的。在我的最新專案中,我重構了一個 Angular 應用程式,使用 Veo 3.1 模型生成高品質影片,並完全透過 Firebase 服務進行管理。

透過將此邏輯移至伺服器端,我們可以保護我們的 API 金鑰,並且可以在不重新部署整個使用者介面的情況下更新模型參數。

先決條件

專案的技術堆疊:

  • Angular 21:截至 2025 年 12 月的最新版本。
  • Node LTS:使用截至 2025 年 12 月的 LTS 版本。
  • Firebase 遠端配置 (Remote Config):用於管理動態參數。
  • Firebase 雲端函式:供前端呼叫以生成影片或在兩張影像之間進行影片內插(interpolation)。
  • Firebase 雲端儲存 (Cloud Storage):在預設的 Firebase 儲存貯體中代管生成的影片檔案。
  • Firebase 雲端函式 模擬器:在 http://localhost:5001 本地測試函式。
  • Gemini in Vertex AI:使用 Vertex AI 中的 Gemini 生成影片並將其儲存於 Firebase 雲端儲存。

公用的 Google AI Studio API 在我的地區(香港)受限。然而,Vertex AI(Google Cloud)提供企業級存取權限,在當地運作穩定,因此我在本次示範中選擇了 Vertex AI。

npm i -g firebase-tools

使用 npm 全域安裝 firebase-tools。

firebase logout
firebase login

登出 Firebase 並重新登入以執行正確的 Firebase 驗證。

firebase init

執行 firebase init 並依照畫面指示設定 Firebase 雲端函式、Firebase Emulator Suite、Firebase 雲端儲存與 Firebase 遠端配置。

如果您已有專案或有多個專案,可以在命令列中指定專案 ID。

firebase init --project <PROJECT_ID>

在上述兩種情況下,Firebase CLI 都會自動安裝 firebase-admin 與 firebase-functions 相依性。

完成設定步驟後,Firebase 工具將生成函式模擬器、函式、儲存規則檔案、遠端配置模板,以及如 .firebaserc 和 firebase.json 等設定檔。

  • Angular 相依性
npm i firebase

Angular 應用程式需要 firebase 相依性來初始化 Firebase 應用程式、載入遠端配置,並呼叫 Firebase 雲端函式以生成影片。

  • Firebase 相依性
npm i @cfworker/json-schema @google/genai @modelcontextprotocol/sdk

安裝上述相依性以存取 Vertex AI 中的 Gemini。@google/genai 相依於 @cfworker/json-schema 與 @modelcontextprotocol/sdk。如果不安裝它們,雲端函式將無法啟動。


架構

影片生成流程的高階架構

前端應用程式是使用 Angular 建置的。它依賴 Firebase AI Logic 並使用 Gemini 3 Image Pro Preview 模型來生成影像。接著,文字提示詞(prompt)與影像會被提交至 Firebase 雲端函式以生成影片。Firebase AI Logic 不支援影片生成,因此雲端函式會呼叫 Gemini API 與 Veo 3.1 模型來建立影片。此外,Gemini API 允許使用 outputGcsUri 參數,該參數是一個帶有 gs:// 前綴的有效 Google Cloud Storage 路徑。此函式將生成的影片儲存在指定的儲存貯體中並傳回 GCS URI。用戶端將 GCS URI 解析為 HTTP URL,並在 HTML 影片播放器元件中播放該影片。


Firebase 整合

1. 設定環境變數

我在 Firebase 專案中定義環境變數。這可確保函式知道用於儲存與函式代管的區域,以及用於影片生成的 Veo 模型。

.env.example

GOOGLE_CLOUD_LOCATION="us-central1"
GOOGLE_GENAI_USE_VERTEXAI=true
GEMINI_VIDEO_MODEL_NAME="veo-3.1-fast-generate-001"
IS_VEO31_USED="true"
POLLING_PERIOD_MS="10000"
GOOGLE_FUNCTION_LOCATION="us-central1"
WHITELIST="http://localhost:4200"
REFERER="http://localhost:4200/"
變數 說明
GOOGLE_CLOUD_LOCATION 儲存貯體的位置。我選擇 us-central1,因為該區域的儲存貯體始終免費。
GOOGLE_GENAI_USE_VERTEXAI 是否使用 Vertex AI。
GEMINI_VIDEO_MODEL_NAME Gemini 影片模型的名稱。
IS_VEO31_USED 是否使用 Veo 3.1。若為 false,則回退(fallback)至生成影片而非內插。
POLLING_PERIOD_MS 影片操作的輪詢(polling)週期(毫秒)。
GOOGLE_FUNCTION_LOCATION 雲端函式的區域。我選擇 us-central1,以便讓函式與儲存貯體位在同一區域。
WHITELIST 請求必須來自 http://localhost:4200
REFERER 請求源自於 http://localhost:4200/

2. 驗證環境變數

在雲端函式繼續執行任何 AI 呼叫之前,確保所有必要的環境變數都存在至關重要。我實作了一個 validateVideoConfigFields 協助函式(helper function)來檢查是否使用 Veo 3.1、輪詢週期、是否使用 Vertex AI、Vertex AI 位置、模型名稱以及專案 ID。

import logger from "firebase-functions/logger";

export function validate(value: string | undefined, fieldName: string, missingKeys: string[]) {
  const err = `${fieldName} is missing.`;
  if (!value) {
    logger.error(err);
    missingKeys.push(fieldName);
    return "";
  }

  return value;
}
import { GenerateVideosParameters, GoogleGenAI } from "@google/genai";
import { validate } from "../validate";

export function validateVideoConfigFields() {
  process.loadEnvFile();

  const env = process.env;
  const isVeo31Used = (env.IS_VEO31_USED || "false") === "true";
  const pollingPeriod = Number(env.POLLING_PERIOD_MS || "10000");
  const vertexai = (env.GOOGLE_GENAI_USE_VERTEXAI || "false") === "true";

  const missingKeys: string[] = [];
  const location = validate(env.GOOGLE_CLOUD_LOCATION, "Vertex Location", missingKeys);
  const model = validate(env.GEMINI_VIDEO_MODEL_NAME, "Gemini Video Model Name", missingKeys);
  const project = validate(env.GOOGLE_CLOUD_QUOTA_PROJECT, "Project ID", missingKeys);

  if (missingKeys.length > 0) {
    throw new Error(`Missing environment variables: ${missingKeys.join(", ")}`);
  }

  return {
    genAIOptions: {
      project,
      location,
      vertexai,
    },
    aiVideoOptions: {
      model,
      storageBucket: `${project}.firebasestorage.app`,
      isVeo31Used,
      pollingPeriod,
    },
  };
}

我使用的是 2025 年 12 月的 Node 24。自 Node 20 起,我們可以使用內建的 process.loadEnvFile 函式,從 .env 檔案載入環境變數。

如果您使用的 Node 版本不支援 process.loadEnvfile,替代方案是安裝 dotenv 來載入環境變數。

npm i dotenv
import dotenv from "dotenv";

dotenv.config();

Firebase 提供 GOOGLE_CLOUD_QUOTA_PROJECT 變數,因此它未定義在 .env 檔案中。

當 missingKeys 陣列不為空時,函式會拋出一個錯誤,列出所有缺失的變數名稱。如果驗證成功,則會傳回 genAIOptions 與 aiVideoOptions。genAIOptions 用於初始化 GoogleGenAI,而 aiVideoOptions 則包含影片生成與內插的參數。

3. 生成影片並儲存至 Firebase Storage

generateVideo 雲端函式將酬載(payload)傳遞給 generateVideoFunction 函式。

所有雲端函式都強制執行 App Check、CORS 以及 180 秒的逾時期間。如果未指定 WHITELIST,則 CORS 預設為 true。在示範中這沒問題,但在正式環境中,預設為 false 或特定網域會更安全。

const cors = process.env.WHITELIST ? process.env.WHITELIST.split(",") : true;
const options = {
  cors,
  enforceAppCheck: true,
  timeoutSeconds: 180,
};

export const generateVideo = onCall( options,
  ({ data }) => generateVideoFunction(data)
);

generateVideoFunction 委派給 generateVideoURL 函式來建構影片參數,並輪詢影片操作直到其完成。最後,它傳回 GCS URI 或拋出錯誤。

import { GoogleGenAI } from "@google/genai";
import { AIVideoBucket, GenerateVideoRequest } from "./types/video.type";
import { generateVideoByPolling, validateVideoConfigFields } from "./video.util";

export async function generateVideoFunction(data: GenerateVideoRequest) {
  const variables = validateVideoConfigFields();
  if (!variables) {
    return "";
  }

  const { genAIOptions, aiVideoOptions } = variables;

  try {
    const ai = new GoogleGenAI(genAIOptions);
    return await generateVideoURL({ ai, ...aiVideoOptions }, data);
  } catch (error) {
    console.error("Error generating video:", error);
    throw new Error("Error generating video");
  }
}

async function generateVideoURL(aiVideo: AIVideoBucket, imageParams: GenerateVideoRequest) {
  const args = constructVideoArguments(aiVideo.isVeo31Used, imageParams);
  return generateVideoByPolling(aiVideo, args);
}

Veo 3.1 支援解析度(resolution)屬性,可能的值為 1080p 與 720p。為了示範目的,我將解析度硬編碼為 720p。對於 Veo 3 或更舊版本,我省略了解析度。

function constructVideoArguments(isVeo31Used: boolean, imageParams: GenerateVideoRequest) {
  const veoConfig = isVeo31Used ? {
    aspectRatio: "16:9",
    resolution: "720p",
  } : {
    aspectRatio: "16:9",
  };

  return {
    prompt: imageParams.prompt,
    imageBytes: imageParams.imageBytes,
    mimeType: imageParams.mimeType,
    config: veoConfig,
  };
}

4. 非同步輪詢

影片生成與內插都是耗時較長的任務。因為 Vertex AI 是非同步處理這些任務,函式必須輪詢操作狀態,直到 done 旗標為 true。

Gemini API 無法存取 Firebase Emulator 的 Cloud Storage,因此它需要一個真實的輸出 GCS URI,即 gs://${storageBucket}。

import { GenerateVideosConfig, GoogleGenAI } from "@google/genai";

export type AIVideoBucket = {
  ai: GoogleGenAI;
  model: string;
  storageBucket: string;
  isVeo31Used: boolean;
  pollingPeriod: number;
}

export type GenerateVideoRequest = {
  prompt: string;
  imageBytes: string;
  mimeType: string;
  config?: GenerateVideosConfig;
}
import { AIVideoBucket, GenerateVideoRequest } from "./types/video.type";
import { GenerateVideosParameters, GoogleGenAI } from "@google/genai";

export async function generateVideoByPolling(
  { ai, model, storageBucket, pollingPeriod }: AIVideoBucket,
  request: GenerateVideoRequest,
) {
  const genVideosParams: GenerateVideosParameters = {
    model,
    prompt: request.prompt,
    config: {
      ...request.config,
      numberOfVideos: 1,
      outputGcsUri: `gs://${storageBucket}`,
    },
    image: {
      imageBytes: request.imageBytes,
      mimeType: request.mimeType,
    },
  };

  return getVideoUri(ai, genVideosParams, pollingPeriod);
}

當 done 旗標為 true 時,操作結束並會出現以下三種結果之一:
結果 1:error 為 true,影片生成失敗。因此,函式拋出錯誤。
結果 2:影片已儲存在 GCS URI,且函式將其傳回給用戶端應用程式。
結果 3:皆未發生。既無錯誤也無 GCS URI,函式傳回未知錯誤。

async function getVideoUri(
  ai: GoogleGenAI,
  genVideosParams: GenerateVideosParameters,
  pollingPeriod: number,
): Promise<string> {
  let operation = await ai.models.generateVideos(genVideosParams);

  while (!operation.done) {
    await new Promise((resolve) => setTimeout(resolve, pollingPeriod));
    operation = await ai.operations.getVideosOperation({ operation });
  }

  if (operation.error) {
    const strError = `Video generation failed: ${operation.error.message}`;
    throw new Error(strError);
  }

  const uri = operation.response?.generatedVideos?.[0]?.video?.uri;
  if (uri) {
    return uri;
  }

  const strError = "Video generation finished but no uri was provided.";
  throw new Error(strError);
}

注意:為了示範目的,輪詢是處理非同步影片生成的不錯方案。然而,這相當耗費成本,且會產生不必要的負載與延遲。對於正式生產用途,您可能會考慮推播通知(push notification),例如 WebSocket 或伺服器傳送事件(Server-Sent Events)。

5. 影格間的影片內插

Veo 3.1 還引入了影片內插功能,模型使用兩張影像來推論影片中發生的變化。在此模式下,函式會傳送第一張與最後一張影格(frame)。AI 會生成兩者之間的過渡,有效地將序列「動畫化」。

const cors = process.env.WHITELIST ? process.env.WHITELIST.split(",") : true;
const options = {
  cors,
  enforceAppCheck: true,
  timeoutSeconds: 180,
};

export const interpolateVideo = onCall( options,
  ({ data }) => generateVideoFromFramesFunction(data)
);
import { GenerateVideosConfig } from "@google/genai";

export type GenerateVideoRequest = {
  prompt: string;
  imageBytes: string;
  mimeType: string;
  config?: GenerateVideosConfig;
}

export type GenerateVideoFromFramesRequest = GenerateVideoRequest & {
  lastFrameImageBytes: string;
  lastFrameMimeType: string;
}
import { AIVideoBucket, GenerateVideoFromFramesRequest } from "./types/video.type";
import { GoogleGenAI } from "@google/genai";
import { generateVideoByPolling, validateVideoConfigFields } from "./video.util";

export async function generateVideoFromFramesFunction(data: GenerateVideoFromFramesRequest) {
  const variables = validateVideoConfigFields();
  if (!variables) {
    return "";
  }

  const { genAIOptions, aiVideoOptions } = variables;

  try {
    const ai = new GoogleGenAI(genAIOptions);
    return await interpolateVideo({ ai, ...aiVideoOptions }, data);
  } catch (error) {
    console.error("Error generating video:", error);
    throw new Error("Error generating video");
  }
}

目前僅 Veo 3.1 支援 lastFrame。當 isVeo31Used 為 true 時,資料 URL 與 MIME 類型會提供給 lastFrame。否則,函式將回退至從第一張影像生成影片。

function constructVideoArguments(isVeo31Used: boolean, imageParams: GenerateVideoFromFramesRequest) {
  const veoConfig = isVeo31Used ? {
    aspectRatio: "16:9",
    resolution: "720p",
    lastFrame: {
      imageBytes: imageParams.lastFrameImageBytes,
      mimeType: imageParams.lastFrameMimeType,
    },
  } : {
    aspectRatio: "16:9",
  };

  return {
    prompt: imageParams.prompt,
    imageBytes: imageParams.imageBytes,
    mimeType: imageParams.mimeType,
    config: veoConfig,
  };
}

同樣地,interpolateVideo 函式重複使用 generateVideoByPolling 函式來輪詢操作,直到操作完成。函式傳回 GCS URI 或拋出錯誤。

async function interpolateVideo(aiVideo: AIVideoBucket, imageParams: GenerateVideoFromFramesRequest) {
  try {
    const args = constructVideoArguments(aiVideo.isVeo31Used, imageParams);
    return await generateVideoByPolling(aiVideo, args);
  } catch (e) {
    throw e instanceof Error ?
      e :
      new Error("An unexpected error occurred in video generation using the first and last frames.");
  }
}

6. 儲存安全性規則

為了確保生成的內容得到正確處理,Firebase Storage 規則被設定為僅允許 MP4 檔案。

service firebase.storage {
    match /b/{bucket}/o {
      match /{allPaths=**} {
        allow read: if resource.name.matches('.*\\.mp4');
        allow write: if request.resource.name.matches('.*\\.mp4')
                    && request.resource.contentType == 'video/mp4';
      }
    }
}

7. 在 Storage 中清理影片

即使 us-central1 區域提供免費層級,影片也應在一段時間後清除,以免儲存貯體無限增長。

我在 Google Cloud Storage 中新增了一條規則,在物件建立 5 天後從 Firebase Storage 儲存貯體中刪除物件。5 天是我隨機選擇的,您可以根據自己的情境選擇任何天數。

Google Cloud Console > Select the Firebase Project > Cloud Storage > Buckets > Select Bucket name > Lifecycle > Rules > Add a rule
欄位 值
操作 刪除物件
物件條件 建立物件已超過 5 天以上

8. Firebase 應用程式配置與 reCAPTCHA 網站金鑰

getFirebaseConfig 是一個 Firebase 雲端函式,傳回 Firebase 應用程式配置與 reCAPTCHA 網站金鑰。

const cors = process.env.WHITELIST ? process.env.WHITELIST.split(",") : true;
const whitelist = process.env.WHITELIST?.split(",") || [];
const refererList = process.env.REFERER?.split(",") || [];

export const getFirebaseConfig = onRequest( { cors },
  (request, response) => {
    if (request.method !== "GET") {
      response.status(405).send("Method Not Allowed");
      return;
    }

    try {
      const referer = request.header("referer");
      const origin = request.header("origin");
      if (!referer) {
        response.status(401).send("Unauthorized, invalid referer.");
        return;
      }

      if (!refererList.includes(referer)) {
        response.status(401).send("Unauthorized, invalid referer.");
        return;
      }

      if (!origin) {
        response.status(401).send("Unauthorized, invalid origin.");
        return;
      }

      if (!whitelist.includes(origin)) {
        response.status(401).send("Unauthorized, invalid origin.");
        return;
      }

      const config = {
            app: {
                apiKey: '<Firebase API Key>',
                appId: '<Firebase App ID',
                projectId: '<Google Cloud Project ID>',
                storageBucket: '<Firebase Storage Bucket>
                messagingSenderId: '<Firebase Messaging Sender ID>',
                authDomain: '<Firebase Auth Domain>',
            },
            recaptchaSiteKey: '<reCAPTCHA Site Key>',
      };

      response.set("Cache-Control", "public, max-age=3600, s-maxage=3600");
      response.json(config);
    } catch (err) {
      console.error(err);
      response.status(401).send(err);
    }
  }
);

9. 使用模擬器進行本地開發

對於本地開發,我使用了 Firebase Emulator Suite。在 bootstrapFirebase 流程中,應用程式會呼叫 connectFunctionsEmulator 以連結至在 http://localhost:5001 執行的雲端函式。

執行 firebase init 時,埠號預設為 5001。

注意: 雖然雲端函式是在本地執行(零成本),但 Storage 模擬器並未使用。這是因為 Gemini API 需要一個實際可存取的 GCS 儲存貯體來儲存生成的影片。

loadFirebaseConfig 是一個協助函式,向雲端函式發出請求以取得 Firebase 配置與 reCAPTCHA 網站金鑰。

{
  "appUrl": "<Firebase cloud function base URL>"
}
import { connectFunctionsEmulator, Functions, getFunctions } from "firebase/functions";
import { fetchAndActivate, getRemoteConfig, getValue, RemoteConfig } from 'firebase/remote-config';
import { FirebaseApp, initializeApp } from 'firebase/app';
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, lastValueFrom, throwError } from 'rxjs';

async function loadFirebaseConfig() {
  const httpService = inject(HttpClient);
  const firebaseConfig$ =
    httpService.get(`${config.appUrl}/getFirebaseConfig`)
      .pipe(
        catchError((e) => throwError(() => e))
      );
  return lastValueFrom(firebaseConfig$);
}

export async function bootstrapFirebase() {
    try {
      const firebaseConfig = await loadFirebaseConfig();
      const { app, recaptchaSiteKey } = firebaseConfig;
      const firebaseApp = initializeApp(app);

      initializeAppCheck(firebaseApp, {
        provider: new ReCaptchaEnterpriseProvider(recaptchaSiteKey),
        isTokenAutoRefreshEnabled: true,
      });

      const functions = getFunctions(firebaseApp, 'us-central1');
      if (location.hostname === 'localhost') {
        connectFunctionsEmulator(functions, 'localhost', 5001);
      }
    } catch (err) {
      console.error(err);
    }
}

AppConfig 保持不變。


import { ApplicationConfig, provideAppInitializer } from '@angular/core';
import { bootstrapFirebase } from './app.bootstrap';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAppInitializer(async () => bootstrapFirebase()),
  ]
};

10. 前端整合 (Angular)

Angular 前端使用 httpsCallable 觸發流程。一旦函式傳回 Cloud Storage 路徑,應用程式就會擷取下載 URL 進行播放。

ConfigService 儲存要在整個應用程式中使用的 Firebase 應用程式與函式。

import { Injectable } from '@angular/core';
import { FirebaseApp } from 'firebase/app';
import { Functions } from 'firebase/functions';

@Injectable({
  providedIn: 'root'
})
export class ConfigService  {

    firebaseApp: FirebaseApp | undefined = undefined;
    functions: Functions | undefined = undefined;

    loadConfig(firebaseApp: FirebaseApp, functions: Functions) {
      this.firebaseApp = firebaseApp;
      this.functions = functions;
    }
}

retrieveVideoUri 方法直接呼叫雲端函式以擷取 GCS URI。

downloadVideoAsUrl 方法將 URI 解析為 HTTP URL,以便 HTML 影片播放器可以立即播放。

import { inject, Injectable } from '@angular/core';
import { httpsCallable } from 'firebase/functions';
import { getDownloadURL, getStorage, ref } from 'firebase/storage';
import { GenerateVideoRequest } from '../types/video.type';
import { ConfigService } from './config.service';

@Injectable({
  providedIn: 'root'
})
export class GeminiService {
  private readonly storage = getStorage();
  private readonly configService = inject(ConfigService);

  async retrieveVideoUri(request: GenerateVideoRequest, methodName: string): Promise<string> {
    try {
      const functions = this.configService.functions;
      if (!functions) {
        throw new Error('Functions does not exist.');
      }

      const downloadGcsUri = httpsCallable<GenerateVideoRequest, string>(
        functions, methodName
      );
      const { data: gcsUri } = await downloadGcsUri(request);
      return gcsUri;
    } catch (err) {
        console.error(err);
        throw err;
    }
  }

  async downloadVideoAsUrl(request: GenerateVideoRequest, methodName='videos-generateVideo'): Promise<string> {
    const gcsUri = await this.retrieveVideoUri(request, methodName);

    if (!gcsUri) {
      throw new Error('Video operation completed but no URI was returned.');
    }

    return getDownloadURL(ref(this.storage, gcsUri))
      .then((url) => url)
      .catch((error) => {
        console.error(error);
        throw new Error("Unknown error occurred");
      });
  }
}

VideoPlayerComponent 具有必要的 videoUrl 訊號輸入(signal input),該輸入被分配給影片播放器的來源以播放影片。

@Component({
  selector: 'app-video-player',
  template: `
<div>
    <video [src]="videoUrl()" controls autoplay loop class="w-full rounded-md"></video>
</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VideoPlayerComponent {
  isGeneratingVideo = input(false);
  videoUrl = input.required<string>();
}

本示範的逐步解說至此結束,您應該已經能在雲端函式中生成影片、將其安全地儲存在儲存貯體中,並在使用者介面的影片播放器中播放。


結論

將 Veo 3.1 與 Firebase 的無伺服器擴展性相結合是一個強大的工作流程。

首先,Angular 應用程式既不需要安裝 genai 相依性,也不需要在 .env 檔案中維護 Vertex AI 環境變數。用戶端應用程式呼叫雲端函式來執行密集型任務並等待結果。

雲端函式接收來自用戶端的參數,執行如生成與內插等複雜的 AI 操作,並將影片安全地寫入專屬的儲存貯體中。在本地開發期間,Firebase Emulator 會呼叫位在 http://localhost:5001 的函式,而非 Cloud Run 平台上的已部署函式。

該應用程式可以進一步擴充,探索其他 Veo 3.1 功能,例如擴充影片以及使用參考影像生成新影片。由於這些功能目前僅支援 Python,因此必須初始化一個獨立的 Python 程式碼庫,且不覆寫 TypeScript 函式定義。

擴充影片非常有趣,因為 Veo 生成的影片可以再延長 7 秒,最多可延長 20 次。最終影片的總長度最長可達 148 秒(約 8 秒 + 20 * 7 秒),約 2.5 分鐘。

資源


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言